<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>个人网盘 Dashboard</title>
    <link
            rel="stylesheet"
            href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
    />
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
    <script>
        // 配置 axios 默认发送 cookie（同域请求通常会自动发送，但显式配置更安全）
        axios.defaults.withCredentials = true;
        
        // 获取 CSRF token 的辅助函数
        function getCSRFToken() {
            // 从 cookie 中读取 CSRF token
            const cookies = document.cookie.split(';');
            for (let cookie of cookies) {
                const trimmed = cookie.trim();
                const equalIdx = trimmed.indexOf('=');
                if (equalIdx === -1) continue;
                
                const name = trimmed.substring(0, equalIdx);
                const value = trimmed.substring(equalIdx + 1);
                
                if (name === 'csrf_token') {
                    // Cookie 值可能需要 URL 解码（浏览器会自动编码特殊字符）
                    try {
                        return decodeURIComponent(value);
                    } catch (e) {
                        // 如果解码失败，返回原始值
                        return value;
                    }
                }
            }
            return null;
        }
        
        // 添加请求拦截器，自动添加 CSRF token
        axios.interceptors.request.use(
            config => {
                // 只对写操作添加 CSRF token
                const method = config.method?.toUpperCase();
                if (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH') {
                    const csrfToken = getCSRFToken();
                    if (csrfToken) {
                        config.headers['X-CSRF-Token'] = csrfToken;
                    }
                }
                return config;
            },
            error => {
                return Promise.reject(error);
            }
        );
        
        // 添加响应拦截器处理 token 过期和自动刷新
        let isRefreshing = false;
        let failedQueue = [];
        
        const processQueue = (error, token = null) => {
            failedQueue.forEach(prom => {
                if (error) {
                    prom.reject(error);
                } else {
                    prom.resolve(token);
                }
            });
            failedQueue = [];
        };
        
        // 跳转到登录页的辅助函数
        const redirectToLogin = () => {
            // 清除所有 token cookies
            document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
            document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
            document.cookie = 'csrf_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
            // 跳转到登录页
            window.location.href = '/login.html';
        };
        
        axios.interceptors.response.use(
            response => response,
            async error => {
                const originalRequest = error.config;
                
                // 处理 401 未授权错误（token过期或被撤销）
                if (error.response && error.response.status === 401) {
                    // 如果是刷新token的请求本身返回401，直接跳转登录
                    if (originalRequest.url === '/api/v1/refresh') {
                        redirectToLogin();
                        return Promise.reject(error);
                    }
                    
                    // 如果已经在刷新 token，等待刷新完成
                    if (isRefreshing) {
                        return new Promise((resolve, reject) => {
                            failedQueue.push({ resolve, reject });
                        }).then(token => {
                            originalRequest.headers['Authorization'] = 'Bearer ' + token;
                            return axios(originalRequest);
                        }).catch(err => {
                            // 如果刷新失败，跳转登录
                            redirectToLogin();
                            return Promise.reject(err);
                        });
                    }
                    
                    // 避免重复刷新
                    if (originalRequest._retry) {
                        redirectToLogin();
                        return Promise.reject(error);
                    }
                    
                    originalRequest._retry = true;
                    isRefreshing = true;
                    
                    try {
                        // 尝试使用 refresh token 刷新 access token
                        const refreshRes = await axios.post('/api/v1/refresh', {}, {
                            withCredentials: true
                        });
                        
                        const { access_token, csrf_token } = refreshRes.data;
                        
                        // 更新 CSRF token cookie（如果返回了新的）
                        if (csrf_token) {
                            document.cookie = `csrf_token=${csrf_token}; path=/; max-age=86400`;
                        }
                        
                        // 处理等待队列
                        processQueue(null, access_token);
                        isRefreshing = false;
                        
                        // 重试原始请求（使用新的 token）
                        return axios(originalRequest);
                    } catch (refreshError) {
                        // 刷新失败，清除所有 token 并跳转登录
                        processQueue(refreshError, null);
                        isRefreshing = false;
                        
                        redirectToLogin();
                        return Promise.reject(refreshError);
                    }
                }
                
                return Promise.reject(error);
            }
        );
        
        // 计算文件 SHA-256，返回 hex 字符串
        async function computeSHA256(file) {
            const buffer = await file.arrayBuffer()
            const hashBuffer = await crypto.subtle.digest('SHA-256', buffer)
            const byteArray = Array.from(new Uint8Array(hashBuffer))
            return byteArray.map(b => b.toString(16).padStart(2, '0')).join('')
        }
    </script>
</head>
<body>
<div id="app" class="container mt-4">
    <!-- 顶部栏 -->
    <div class="d-flex justify-content-between align-items-center mb-2">
        <h2>GoPan 网盘</h2>
        <div>
            <button class="btn btn-outline-info btn-sm me-2" @click="toggleTrash">
                {{ showTrash ? '返回文件' : '🗑️ 回收站' }}
            </button>
            <span class="me-3">用户：{{ username }}</span>
            <button class="btn btn-outline-danger btn-sm" @click="logout">退出</button>
        </div>
    </div>

    <!-- 面包屑导航 -->
    <nav aria-label="breadcrumb" class="mb-3" v-if="!showTrash">
        <ol class="breadcrumb">
            <li class="breadcrumb-item" :class="{active: currentParentId === 0}" aria-current="page">
                <a v-if="currentParentId !== 0" href="#" @click.prevent="goRoot">根目录</a>
                <span v-else>根目录</span>
            </li>
            <li v-for="(bc, idx) in breadcrumbs" :key="bc.id" class="breadcrumb-item" :class="{active: idx === breadcrumbs.length - 1}" aria-current="page">
                <a v-if="idx !== breadcrumbs.length - 1" href="#" @click.prevent="goToBreadcrumb(idx)">{{ bc.name }}</a>
                <span v-else>{{ bc.name }}</span>
            </li>
        </ol>
    </nav>

    <!-- 文件工具栏 -->
    <div class="mb-3 d-flex gap-2 align-items-center" v-if="!showTrash">
        <input type="file" @change="handleFileChange"/>
        <button class="btn btn-outline-secondary btn-sm" @click="createDir">新建文件夹</button>
    </div>

    <!-- 文件列表 -->
    <div v-if="showTrash" class="alert alert-info mb-3">
        <strong>回收站</strong> - 已删除的文件会在这里保留 30 天，之后将自动永久删除
    </div>
    <table class="table table-bordered table-hover">
        <thead class="table-light">
        <tr>
            <th>文件名</th>
            <th>大小</th>
            <th>类型</th>
            <th v-if="showTrash">删除时间</th>
            <th>操作</th>
        </tr>
        </thead>
        <tbody>
        <tr v-for="file in (showTrash ? trashFiles : files)" :key="file.id" 
            :style="{cursor: showTrash ? 'default' : 'pointer'}" 
            :class="{'table-secondary': showTrash}"
            :draggable="!showTrash"
            @dblclick="openItem(file)"
            @dragstart="handleDragStart(file, $event)"
            @dragend="handleDragEnd($event)"
            @dragover.prevent="handleDragOver($event)"
            @drop="handleDrop(file, $event)">
            <td>
                <span v-if="file.is_dir">📁</span>
                <span v-else>📄</span>
                &nbsp;{{ file.name }}
            </td>
            <td>{{ formatSize(file.size) }}</td>
            <td>{{ file.mime_type }}</td>
            <td v-if="showTrash">{{ formatDate(file.deleted_at) }}</td>
            <td>
                <template v-if="!showTrash">
                    <button class="btn btn-sm btn-primary me-2" @click="downloadFile(file)">下载</button>
                    <button class="btn btn-sm btn-secondary me-2" @click="openShare(file)">分享</button>
                    <button class="btn btn-sm btn-danger" @click="deleteFile(file)">删除</button>
                </template>
                <template v-else>
                    <button class="btn btn-sm btn-success me-2" @click="restoreFile(file)">恢复</button>
                    <button class="btn btn-sm btn-danger" @click="hardDeleteFile(file)">彻底删除</button>
                </template>
            </td>
        </tr>
        <tr v-if="(showTrash ? trashFiles : files).length === 0">
            <td colspan="5" class="text-center text-muted">
                {{ showTrash ? '回收站为空' : '暂无文件' }}
            </td>
        </tr>
        </tbody>
    </table>

    <!-- 分享弹框（使用 Bootstrap 样式，手动控制显示） -->
    <div v-if="showShareModal" class="modal d-block" tabindex="-1">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">分享文件</h5>
            <button type="button" class="btn-close" @click="closeShareModal"></button>
          </div>
          <div class="modal-body">
            <div class="mb-3">
              <label class="form-label">有效期（小时，可留空表示不过期）</label>
              <input type="number" min="0" class="form-control" v-model="shareHours" placeholder="例如 24" />
            </div>
            <div class="mb-3">
              <div class="form-check">
                <input class="form-check-input" type="checkbox" id="shareHasPassword" v-model="shareHasPassword">
                <label class="form-check-label" for="shareHasPassword">
                  设置访问密码
                </label>
              </div>
            </div>
            <div class="mb-3" v-if="shareHasPassword">
              <label class="form-label">访问密码（4位数字）</label>
              <input type="text" class="form-control" v-model="sharePassword" 
                     maxlength="4" pattern="[0-9]{4}" placeholder="请输入4位数字密码" />
              <div class="form-text">密码用于保护分享链接，访问时需要输入</div>
            </div>
            <div class="mb-2" v-if="shareLink">
              <label class="form-label">分享链接</label>
              <div class="input-group">
                <input type="text" class="form-control" :value="shareLink" readonly>
                <button class="btn btn-outline-primary" type="button" @click="copyShareLink">复制</button>
              </div>
              <div class="form-text" v-if="copyOk" style="color: #2e7d32;">已复制到剪贴板</div>
            </div>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" @click="closeShareModal">关闭</button>
            <button type="button" class="btn btn-primary" :disabled="shareBusy" @click="createShareLink">{{ shareBusy ? '生成中...' : '生成链接' }}</button>
          </div>
        </div>
      </div>
    </div>
    <div v-if="showShareModal" class="modal-backdrop fade show"></div>
</div>

<script>
    const {createApp} = Vue

    createApp({
        data() {
            return {
                username: '',
                files: [],
                trashFiles: [],
                currentParentId: 0,
                breadcrumbs: [], // [{id, name}]
                showTrash: false,
                // 分享弹框状态
                showShareModal: false,
                shareTarget: null,
                shareHours: '',
                shareLink: '',
                shareBusy: false,
                copyOk: false,
                shareHasPassword: false,
                sharePassword: '',
                // 拖拽状态
                draggedFile: null,
                selectedFiles: new Set(),
            }
        },
        mounted() {
            this.fetchUser()
            this.fetchFiles()
        },
        methods: {
            // 从 cookie 获取 CSRF token 的辅助方法
            getCSRFTokenFromCookie() {
                const cookies = document.cookie.split(';');
                for (let cookie of cookies) {
                    const trimmed = cookie.trim();
                    const equalIdx = trimmed.indexOf('=');
                    if (equalIdx === -1) continue;
                    
                    const name = trimmed.substring(0, equalIdx);
                    const value = trimmed.substring(equalIdx + 1);
                    
                    if (name === 'csrf_token') {
                        // Cookie 值可能需要 URL 解码（浏览器会自动编码特殊字符）
                        try {
                            return decodeURIComponent(value);
                        } catch (e) {
                            // 如果解码失败，返回原始值
                            return value;
                        }
                    }
                }
                return null;
            },
            async fastUploadCheck(file) {
                const hash = await computeSHA256(file)
                const body = {
                    name: file.name,
                    size: file.size,
                    mime_type: file.type || '',
                    hash_sha256: hash,
                    parent_id: 0
                }
                const res = await axios.post('/api/v1/file/FastUploadCheck', body)
                return res.data
            },
            openShare(file) {
                this.shareTarget = file
                this.shareHours = ''
                this.shareLink = ''
                this.copyOk = false
                this.shareHasPassword = false
                this.sharePassword = ''
                this.showShareModal = true
            },
            async createShareLink() {
                if (!this.shareTarget) return
                
                // 验证密码格式
                if (this.shareHasPassword && this.sharePassword.length !== 4) {
                    alert('密码必须是4位数字')
                    return
                }
                
                this.shareBusy = true
                this.copyOk = false
                let expired_at = ''
                const hoursNum = Number(this.shareHours)
                if (!isNaN(hoursNum) && hoursNum > 0) {
                    const ms = hoursNum * 3600 * 1000
                    expired_at = new Date(Date.now() + ms).toISOString()
                }
                
                const requestBody = {
                    file_id: this.shareTarget.id,
                    expired_at: expired_at
                }
                
                // 如果设置了密码，添加到请求体
                if (this.shareHasPassword && this.sharePassword) {
                    requestBody.password = this.sharePassword
                }
                
                try {
                    const res = await axios.post('/api/v1/file/share', requestBody)
                    this.shareLink = location.origin + res.data.link
                } catch (e) {
                    console.error('创建分享失败', e)
                    alert('创建分享失败: ' + (e.response?.data?.error || e.message))
                } finally {
                    this.shareBusy = false
                }
            },
            async copyShareLink() {
                if (!this.shareLink) return
                try {
                    await navigator.clipboard.writeText(this.shareLink)
                    this.copyOk = true
                } catch (e) {
                    this.copyOk = false
                }
            },
            closeShareModal() {
                this.showShareModal = false
                this.shareTarget = null
                this.shareHours = ''
                this.shareLink = ''
                this.copyOk = false
                this.shareHasPassword = false
                this.sharePassword = ''
            },
            fetchUser() {
                axios.get('/api/v1/user/info').then(res => {
                    this.username = res.data.username
                }).catch(err => {
                    console.error('获取用户信息失败:', err)
                    // 如果获取用户信息失败，可能是未登录，跳转到登录页
                    if (err.response?.status === 401) {
                        window.location.href = '/login.html'
                    }
                })
            },
            fetchFiles() {
                axios.get('/api/v1/file/list', { params: { parent_id: this.currentParentId } }).then(res => {
                    this.files = res.data.files;
                }).catch(err => {
                    alert('获取文件列表失败: ' + err.response?.data?.error || err.message)
                })
            },
            goRoot() {
                this.currentParentId = 0
                this.breadcrumbs = []
                this.fetchFiles()
            },
            goToBreadcrumb(index) {
                // keep 0..index
                const target = this.breadcrumbs[index]
                this.currentParentId = target.id
                this.breadcrumbs = this.breadcrumbs.slice(0, index + 1)
                this.fetchFiles()
            },
            enterDir(dir) {
                if (!dir || !dir.is_dir) return
                this.breadcrumbs.push({ id: dir.id, name: dir.name })
                this.currentParentId = dir.id
                this.fetchFiles()
            },
            async createDir() {
                const name = prompt('请输入文件夹名称')
                if (!name) return
                try {
                    await axios.post('/api/v1/file/mkdir', { name, parent_id: this.currentParentId })
                    this.fetchFiles()
                } catch (err) {
                    alert('新建文件夹失败: ' + (err.response?.data?.error || err.message))
                }
            },
            handleFileChange(event) {
                const file = event.target.files[0];
                if (file) {
                    this.uploadFile(file);
                }
            },
            async initUpload(file) {
                const csrfToken = this.getCSRFTokenFromCookie();
                const res = await axios.post('/api/v1/file/InitUpload', {
                    file_name: file.name,
                    file_size: file.size,
                    mime_type: file.type,
                    parent_id: this.currentParentId
                }, {
                    headers: {
                        'X-CSRF-Token': csrfToken || ''
                    }
                });

                return res.data; // 应返回 { upload_id, chunk_size, total_chunks... }
            },
            async uploadChunks(file, uploadId, chunkSize = 2 * 1024 * 1024) {
                const totalChunks = Math.ceil(file.size / chunkSize);
                const csrfToken = this.getCSRFTokenFromCookie();
                
                // 使用流式 MD5 计算，避免将所有 chunk 读入内存
                let md5Hasher = null;
                if (typeof SparkMD5 !== 'undefined') {
                    md5Hasher = new SparkMD5.ArrayBuffer();
                }

                for (let i = 0; i < totalChunks; i++) {
                    const start = i * chunkSize;
                    const end = Math.min(start + chunkSize, file.size);
                    const chunk = file.slice(start, end);

                    const formData = new FormData();
                    formData.append('file', chunk);
                    formData.append('upload_id', uploadId)
                    formData.append('chunk_index', i)
                    formData.append('size', chunk.size)
                    // 将 CSRF token 添加到 FormData（后端会从表单中读取）
                    if (csrfToken) {
                        formData.append('csrf_token', csrfToken)
                    }

                    await axios.post('/api/v1/file/UploadChunk', formData, {
                        headers: {
                            // 不要手动设置 Content-Type，让 axios 自动添加 boundary
                            'X-CSRF-Token': csrfToken || '', // 同时添加到请求头
                            'size': chunk.size
                        },
                        timeout: 300000 // 5分钟超时，大文件上传需要更长时间
                    });
                    
                    // 流式更新 MD5：每个 chunk 处理完后立即追加到 hash，然后释放内存
                    if (md5Hasher) {
                        const chunkArrayBuffer = await chunk.arrayBuffer();
                        md5Hasher.append(chunkArrayBuffer);
                        // chunkArrayBuffer 会自动被 GC 回收，不需要手动释放
                    }
                    
                    // 每上传 10 个 chunk 显示一次进度
                    if ((i + 1) % 10 === 0 || i === totalChunks - 1) {
                        console.log(`Uploaded ${i + 1}/${totalChunks} chunks (${((i + 1) / totalChunks * 100).toFixed(1)}%)`);
                    }
                }
                
                // 返回最终的 MD5 hash
                if (md5Hasher) {
                    return md5Hasher.end();
                } else {
                    // 备用方案：如果没有 MD5 库，返回空字符串，让后端计算
                    console.warn('SparkMD5 not found, using backend-calculated hash');
                    return '';
                }
            },
            async completeUpload(uploadId, hash) {
                const csrfToken = this.getCSRFTokenFromCookie();
                await axios.post(`/api/v1/file/CompleteUpload/${uploadId}?parent_id=${this.currentParentId}&hash=${encodeURIComponent(hash)}`, {}, {
                    headers: {
                        'X-CSRF-Token': csrfToken || ''
                    }
                });
            },
            async uploadFile(file) {
                // 秒传校验
                try {
                    const fast = await this.fastUploadCheck(file)
                    if (fast && fast.instant) {
                        // 直接完成
                        this.fetchFiles()
                        return
                    }
                } catch (e) {
                    console.warn('Fast upload check failed, fallback to normal upload', e)
                }

                const initRes = await this.initUpload(file);
                console.log('InitUpload response:', initRes)
                console.log('upload_id:', initRes.upload_id, 'type:', typeof initRes.upload_id)
                // upload_id 是字符串形式，避免 JavaScript 大整数精度丢失
                const uploadId = initRes.upload_id;
                const chunkSize = initRes.chunk_size || 2 * 1024 * 1024;

                // 上传分片并获取流式 hash
                const streamingHash = await this.uploadChunks(file, uploadId, chunkSize);
                console.log('Streaming hash:', streamingHash);
                
                // 传递 hash 完成上传
                await this.completeUpload(uploadId, streamingHash);

                console.log('Upload complete!');
                this.fetchFiles(); // 刷新文件列表
            },
            openItem(file) {
                if (file.is_dir) {
                    this.enterDir(file)
                } else {
                    this.downloadFile(file)
                }
            },
            downloadFile(file) {
                window.open(`/api/v1/file/download/${file.id}`, '_blank')
            },
            deleteFile(file) {
                if (!confirm(`确定删除文件 "${file.name}" 吗？`)) return
                axios.delete(`/api/v1/file/delete/${file.id}`).then(() => {
                    this.fetchFiles()
                }).catch(err => {
                    alert('删除失败: ' + err.response?.data?.error || err.message)
                })
            },
            async logout() {
                try {
                    // 调用后端登出接口，让后端清除cookie并记录日志
                    await axios.post('/api/v1/logout', {}, {
                        withCredentials: true // 确保发送cookie
                    })
                } catch (err) {
                    // 即使后端调用失败，也清除前端的cookie
                    console.error('Logout API call failed:', err)
                } finally {
                    // 清除所有前端的cookie（双重保险）
                    document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
                    document.cookie = 'refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
                    document.cookie = 'csrf_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
                    // 重定向到登录页面
                    window.location.href = '/login.html'
                }
            },
            formatSize(bytes) {
                if (bytes < 1024) return bytes + ' B'
                if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
                return (bytes / 1024 / 1024).toFixed(1) + ' MB'
            },
            formatDate(dateStr) {
                if (!dateStr) return '-'
                const date = new Date(dateStr)
                return date.toLocaleString('zh-CN', {
                    year: 'numeric',
                    month: '2-digit',
                    day: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    second: '2-digit'
                })
            },
            toggleTrash() {
                this.showTrash = !this.showTrash
                if (this.showTrash) {
                    this.fetchTrashFiles()
                } else {
                    this.fetchFiles()
                }
            },
            fetchTrashFiles() {
                axios.get('/api/v1/file/trash').then(res => {
                    this.trashFiles = res.data.files || []
                }).catch(err => {
                    alert('获取回收站文件列表失败: ' + (err.response?.data?.error || err.message))
                })
            },
            restoreFile(file) {
                if (!confirm(`确定恢复文件 "${file.name}" 吗？`)) return
                axios.post(`/api/v1/file/restore/${file.id}`).then(() => {
                    alert('恢复成功')
                    this.fetchTrashFiles()
                }).catch(err => {
                    alert('恢复失败: ' + (err.response?.data?.error || err.message))
                })
            },
            hardDeleteFile(file) {
                if (!confirm(`确定彻底删除文件 "${file.name}" 吗？此操作不可恢复！`)) return
                axios.delete(`/api/v1/file/hard-delete/${file.id}`).then(() => {
                    alert('已彻底删除')
                    this.fetchTrashFiles()
                }).catch(err => {
                    alert('彻底删除失败: ' + (err.response?.data?.error || err.message))
                })
            },
            handleDragStart(file, event) {
                this.draggedFile = file
                event.dataTransfer.effectAllowed = 'move'
                event.dataTransfer.setData('text/plain', file.id)
            },
            handleDragEnd(event) {
                // 恢复所有元素的透明度
                event.currentTarget.style.opacity = '1'
                this.draggedFile = null
            },
            handleDragOver(event) {
                event.preventDefault()
                event.dataTransfer.dropEffect = 'move'
            },
            handleDrop(dropTargetFile, event) {
                event.preventDefault()
                
                // 检查拖拽的是目录，目标也是目录
                if (!this.draggedFile) return
                
                if (!dropTargetFile.is_dir) {
                    alert('只能拖拽到目录中')
                    return
                }
                
                // 检查是否拖拽到自己或子目录
                if (this.draggedFile.is_dir && this.draggedFile.id === dropTargetFile.id) {
                    alert('不能移动目录到自身')
                    return
                }
                
                // 执行单个文件移动
                this.moveFileTo(this.draggedFile.id, dropTargetFile.id)
                this.draggedFile = null
            },
            async moveFileTo(fileID, targetParentID) {
                try {
                    await axios.post('/api/v1/file/move', {
                        file_id: fileID,
                        new_parent_id: targetParentID
                    })
                    this.fetchFiles()
                } catch (err) {
                    // 静默失败，不提示用户
                }
            },
            toggleFileSelection(file) {
                if (this.selectedFiles.has(file.id)) {
                    this.selectedFiles.delete(file.id)
                } else {
                    this.selectedFiles.add(file.id)
                }
            },
            async batchMoveSelected(targetParentID) {
                if (this.selectedFiles.size === 0) {
                    alert('请先选择要移动的文件')
                    return
                }
                
                const fileIDs = Array.from(this.selectedFiles)
                try {
                    await axios.post('/api/v1/file/batch-move', {
                        file_ids: fileIDs,
                        new_parent_id: targetParentID
                    })
                    this.selectedFiles.clear()
                    this.fetchFiles()
                } catch (err) {
                    // 静默失败，不提示用户
                }
            }
        }
    }).mount('#app')
</script>
</body>
</html>
